home *** CD-ROM | disk | FTP | other *** search
/ Scene 96 / Scene 96 International Edition (Zyklop Software) (Disc 2) (1997).iso / misc / coding / vgacodng / part04.txt < prev    next >
Text File  |  1996-08-07  |  16KB  |  359 lines

  1.  
  2.                              VGA-Kurs - Part #4
  3.  
  4. Wir kommen nun zu "T.C.P.'s Beginner's Guide to VGA Coding", Part IV.
  5. Noch eine Anmerkung: Ich versuche immer, auch Lesern, die die ersten Teile des
  6. Kurses verpaßt haben, die Möglichkeit zu geben, die Beispiele zu nutzen, auch
  7. wenn ihnen die wichtigen Prozeduren fehlen. Der Nachteil ist, daß ich z.B.
  8. die schnelle PutPixel-Prozedur durch einen langsameren Speicherzugriff via
  9. Mem ersetzen muß.
  10. Nun eine Frage: Soll ich es so beibehalten oder in jedem Teil die benötigten
  11. Prozeduren dazuschreiben?
  12. Für alle, die die ersten Teile des Kurses haben: Ihr solltet auf jeden Fall
  13. folgende Prozeduren immer bereit halten:
  14. PutPixel, WaitRetrace, ClrVGA. Ihr könnt diese Prozeduren dann in die Listings
  15. einsetzen.
  16. In diesem Teil werden wir Sprites behandeln, als Bonus gibt es einen kleinen
  17. Abschnitt über Code-Optimierung.
  18. Auf dem PC sind Sprites immer noch für viele ein Mythos, die vorher auf
  19. Homecomputern wie Amiga/Atari/C64 gecodet haben, wo die Maschine alle
  20. Sprite-Operationen erledigt.
  21. Beim PC dagegen war am Anfang nie vorgesehen, daß er einmal auch nur ein
  22. Sprite über den Bildschirm flitzen lassen würde. Deshalb baute man solch eine
  23. Funktion auch nie in die PC-Grafikkarten ein. Das Ergebnis ist, daß wenn man
  24. auf der VGA-Karte Sprites zaubern will, sich um alles selbst zu kümmern hat:
  25. Darstellung, Bewegung, Kollisionsabfragen, Clipping, Durchscheinen des
  26. Hintergrundes, runde Sprites etc.
  27. Das dies erheblich auf die CPU geht, ist abzusehen, denn die VGA-Karte trägt
  28. kein bischen dazu bei.
  29. Allerdings kann man sich einiger Tricks behelfen, wie wir noch sehen werden.
  30. Zu allererst sollte man sich einen Sprite-Editor zulegen. Davon gibt es
  31. unzählige als Shareware, man kann aber auch ein Malprogram wie DPaint
  32. heranziehen.
  33. Nun muß man die Sprites in ein Format bekommen, das man von Pascal aus leicht
  34. lesen kann. Hierbei ist es günstig, wenn man einen Sprite-Editor hat, der die
  35. Sprites als RAW-Dump abspeichert. D.h., daß die Pixel-Informationen
  36. hintereinander in einer Datei abgelegt werden. Zeichnet man z.B. ein 32x32
  37. Pixel großes Sprite, so hat man eine 1024 Byte große Datei, die man direkt
  38. einlesen kann:
  39.  
  40. type Sprite = array[0..1023] of byte;
  41. var f : file;
  42.     Spr : Sprite;
  43. procedure Readsprite(str:string;s:Sprite);
  44. begin
  45.   assign(f,str);
  46.   reset(f,1);
  47.   blockread(f,s,1024);
  48.   close(f);
  49. end;
  50.  
  51. Diese Prozedur liest durch die Blockread-Prozedur 1024 Byte aus der Datei mit
  52. dem Namen Str in die übergebene Variable des Typs Sprite.
  53. Anmerkung: Wer den Autodesk Animator besitzt, kann die Sprites im CEL-Format
  54. speichern. Dieses hat allerdings noch einen 800 Byte großen Header, der
  55. per "seek(f,800)" übersprungen werden muß.
  56. Es gibt aber auch Sprite-Editoren, die die Sprites gleich als Pascal-Source
  57. speichern können (z.B. YC's Sprite Editor, den ich benutze). Dies hat dann
  58. die folgende Form:
  59.  
  60. const Spr : Sprite = (
  61. 0,1,2,3,5,128,255,200,50,20,...
  62. );
  63.  
  64. Nun, da wir hoffentlich unser Sprite in der Variablen Spr haben, wird es Zeit,
  65. es auf den Bildschirm zu bringen:
  66.  
  67. procedure ShowSprite(x,y:word;s:Sprite);
  68. var n1,n2 : byte;
  69. begin
  70.   for n1 := 0 to 31 do
  71.     for n2 := 0 to 31 do mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
  72. end;
  73.  
  74. Dies ist die einfachste Form der ShowSprite-Routine. Kein Clipping, kein
  75. Durchscheinen, keine runden Sprites und das Wichtigste: Keine Geschwindigkeit!
  76. Hier die Funktionsweise der Prozedur: In der Schleife wird ein 32x32 Pixel
  77. großes Sprite an die Koordinaten (X,Y) gesetzt. Dazu wird die Adresse im
  78. Bildschirmspeicher nach der bekannten Formel Adresse = 320 * Y + X berechnet
  79. und dann die Position im Sprite-Array mittels Position = 32 * n1 + n2
  80. dazugezählt. Daraus ergibt sich die Formel Adresse = (n1+Y) * 320 + X + n2.
  81. Schön und gut, aber was ist, wenn das Sprite rund ist, z.B. ein Fußball.
  82. Wenn ich das Sprite auf einen schwarzen Hintergrund setze, macht es nichts,
  83. aber wenn ich einen Hintergrund, wie z.B. ein Fußballfeld habe, und das Sprite
  84. darauf setze, dann habe ich um den Ball einige schwarze Pixel. Das liegt
  85. daran, daß das Array, in dem das Sprite liegt, quadratisch ist, und wenn nun
  86. im Sprite zwar kein Pixel vorgesehen ist, also im Array eine 0 steht, dann
  87. wird trotzdem ein Pixel der Farbe 0 gesetzt.
  88. Noch ein Problem: Hat man ein Sprite, daß an gewissen Stellen durchsichtig
  89. ist, z.B. eine Scheibe Schweizer Käse (Scheiß Beispiel, ich weiß), dann sollte
  90. an den Stellen, wo die Löcher sind, der Hintergrund durchscheinen, tut er aber
  91. nicht, da wieder ein Pixel der Farbe 0 gesetzt wird.
  92. Diese Probleme sind noch leicht zu lösen:
  93.  
  94. procedure ShowSprite(x,y:word;s:Sprite);
  95. var n1,n2 : byte;
  96. begin
  97.   for n1 := 0 to 31 do
  98.     for n2 := 0 to 31 do
  99.       if s[n1*32+n2] <> 0 then mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
  100. end;
  101.  
  102. Findet die Routine nun im Sprite ein Pixel der Farbe 0, so wird es erst gar
  103. nicht gesetzt. Dadurch wird die Prozedur sogar schneller, da einige Pixel des
  104. Sprites übersprungen werden, und so Zeit gespart wird.
  105. Ein weiteres Problem ist, daß wenn das Sprite am Rand des Bildschirms
  106. angelangt ist und über den Rand hinausgeht, es an der anderen Seite des
  107. Bildschirms dargestellt wird. Das liegt am linearen Aufbau des
  108. Bildschirmspeichers. Gehen die Spriteinformationen über den Rand einer Zeile
  109. hinaus, werden sie im Bildschirmspeicher weiter bewegt und erscheinen dadurch
  110. in der nächsten Zeile. Dasselbe passiert, wenn das Sprite den unteren Rand des
  111. Bildschirms überschreitet.
  112. Ein Verfahren zum Vereiteln dieser Tatsache ist, daß vor dem Setzen eines
  113. Pixels überprüft wird, ob er überhaupt noch in der Zeile bzw. Spalte liegt,
  114. also X nicht größer als 319 und Y nicht größer als 199 ist. Dies nennt sich
  115. Clipping.
  116.  
  117. procedure ShowSprite(x,y:word;s:Sprite);
  118. var n1,n2 : byte;
  119. begin
  120.   for n1 := 0 to 31 do
  121.     for n2 := 0 to 31 do
  122.       if (s[n1*32+n2] <> 0) and (n2+x < 320) and (n1+y < 200) then
  123.         mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
  124. end;
  125.  
  126. Damit enthält unsere Prozedur schon eine Menge Ifs und ist deshalb nicht
  127. gerade als die schnellste zu bezeichnen.
  128. Anders wäre das schon bei der Assembler-Version, die zieht sich allerdings
  129. etwas in die Länge...
  130.  
  131. procedure ShowSprite(x,y,add:word);assembler;
  132. asm
  133.   mov     bx,31                        { Zähler mit Endwert initialisieren }
  134. @loop1:                                { für ein 32x32 Pixel Sprite }
  135.   mov     cx,31
  136. @loop2:
  137.   mov     si,bx                        { n1 * 32 + n2 }
  138.   shl     si,5                         { si * 32 }
  139.   add     si,cx
  140.   add     si,add                       { Variablen-Offset drauf }
  141.   cmp     byte ptr ds:[si],0           { Pixelwert = 0 ? }
  142.   je      @next                        { Wenn ja, nächster Pixel }
  143.   mov     ax,cx
  144.   add     ax,x
  145.   cmp     ax,319                       { X-Koordinate > 319 ? }
  146.   ja      @next                        { Wenn ja, nächster Pixel }
  147.   mov     ax,bx
  148.   add     ax,y
  149.   cmp     ax,199                       { Y-Koordinate > 199 ? }
  150.   ja      @next                        { Wenn ja, nächster Pixel }
  151.   mov     ax,bx                        { (n1+y) * 320 + x + n2 }
  152.   add     ax,y
  153.   mov     dx,ax
  154.   shl     ax,6                         { ax * 64 }
  155.   shl     dx,8                         { dx * 256 }
  156.   add     ax,dx
  157.   add     ax,x
  158.   add     ax,cx
  159.   mov     di,ax
  160.   mov     ax,0A000h                    { VGA-Segment nach ES }
  161.   mov     es,ax
  162.   mov     al,ds:[si]                   { Pixelwert aus Sprite-Daten holen }
  163.   mov     es:[di],al                   { und auf VGA-Screen setzen }
  164. @next:
  165.   dec     cx                           { Zähler dekrementieren }
  166.   jnz     @loop2                       { Wenn ungleich 0, innere Schleife }
  167.   dec     bx                           { Zähler dekrementieren }
  168.   jnz     @loop1                       { Wenn ungleich 0, äußere Schleife }
  169. end;
  170.  
  171. Diese Routine schafft immerhin schon rund 3000 Sprites pro Sekunde auf einem
  172. DX/2 80, also ca. doppelt so viel wie die Pascal-Version.
  173. Aber es geht (natürlich) noch schneller.
  174. Die Vorgehensweise der Routine entnehmt ihr bitte den Kommentaren.
  175.  
  176. Folgende Tricks wurden in dieser Routine zur Optimierung verwendet:
  177. 1. Es wird nur das Offset des Sprites übergeben.
  178. Statt das gesamte Sprite als Array an die Prozedur zu übergeben, wird ihr
  179. nur das Offset des Sprites im Speicher mitgeteilt. Dadurch spart man es sich,
  180. ganze 1022 Byte mehr zu übergeben.
  181. Der Prozeduraufruf muß also nicht 'showsprite(x,y,Spritename)' lauten,
  182. sondern 'showsprite(x,y,ofs(Spritename))'.
  183. 2. Es werden so viel Register wie möglich ausgenutzt.
  184. Dies ist eine goldene Regel in der Assembler-Programmierung. Man sollte erst
  185. auf Variablen zurückgreifen, wenn wirklich alle Register ausgenutzt sind.
  186. 3. Die Zähler werden dekrementiert.
  187. Am Anfang werden die Zähler nicht mit dem Start- sondern mit dem Endwert
  188. initialisiert. Danach werden sie in jeder Schleife dekrementiert. Dies hat
  189. den Vorteil, das man sich ein langsames 'cmp zaehler,Endwert' erspart. Es
  190. genügt der bedingte Sprungbefehl 'jnz', der automatisch erneut zum Anfang der
  191. Schleife springt, wenn der Zähler ungleich 0 ist.
  192. 4. Statt 'mul' oder 'div' kommt 'shl' oder 'shr'.
  193. Hat man eine Multiplikation bzw. Division mit bzw. durch einen Faktor der
  194. ein vielfaches von 2 darstellt, kann man die Operation durch Bitverschiebung
  195. beschleunigen. OK, wer hat den Satz verstanden? Niemand? Gut. Also:
  196. Angenommen, man muß (wie in der Prozedur) eine Zahl mit 32 multiplizieren.
  197. Dies geht vor allem auf Prozessoren unter 486 recht träge vonstatten.
  198. Schneller geht es, wenn man die Zahl um 5 nach links shiftet, d.h. alle Bits
  199. der Zahl um 5 Stellen nach links verschiebt. Denn: Eine Verschiebung um 1 Bit
  200. erzeugt dasselbe Ergebnis wie eine Multiplikation mit 2. Ein Shiften um 2
  201. Bits ist wie ein Malnehmen mit 4, 3 Bits wie mal 8, 4 Bits wie mal 16, usw.
  202. Andersrum geht's auch. Ein Shiften um 1 Bit nach rechts ist wie eine Division
  203. durch 2, wobei immer abgerundet wird.
  204. Andere Tricks:
  205. 1. Exklusiv-Oder geschickt einsetzen.
  206. Ein 'mov ax,0' ist identisch mit 'xor ax,ax', bloß mit dem Unterschied, daß
  207. die zweite Art wesentlich schneller geht und als OpCode weniger Bytes
  208. verbraucht.
  209. 2. So viel wie möglich raus aus der Schleife.
  210. In Schleifen sollte nur das stehen, was dort auch wirklich hingehört. Wenn
  211. Schleifen unnötige Befehle wie Variablenzuweisungen oder Rechenoperationen
  212. enthalten, die auch außerhalb der Schleife ihren Dienst verrichten können,
  213. sollte man sie rauswerfen.
  214. 3. Compiler-Optionen ausnutzen.
  215. Folgende Compiler-Befehle am Anfang des Codes beschleunigen das Programm:
  216.  
  217. {$G+,D-,I-,Q-,R-,S-}
  218.  
  219. Damit wird alles, was bremst, abgeschaltet: Debug-Informationen,
  220. In/Out-, Range- und Stack-Checking.
  221.  
  222. So, schön und gut, aber was ist, wenn wir unser Sprite über einen Hintergrund
  223. bewegen wollen? Also, wir setzen das Sprite auf unseren Hintergrund und
  224. löschen es wieder, um es danach an eine andere Stelle zu setzen.
  225. Wuups, da ist ja jetzt ein Loch in unserem schönen Hintergrund!
  226. Tja, um dies zu lösen, könnte man sich Methode 1 oder 2 bedienen. Methode 1
  227. ist, den Hintergrund, auf den das Sprite gesetzt wird, vor dem Setzen zu
  228. sichern, und danach wieder herzustellen, wie in folgendem Schema:
  229.  
  230. var Buffer : Sprite;
  231.     n1,n2  : byte;
  232. begin
  233.   setmcgamode;
  234.   ErzeugeHintergrund;
  235.   repeat
  236.     for n1 := 0 to 31 do               { Ausschnitt sichern }
  237.       for n2 := 0 to 31 do
  238.         Buffer[n1*32+n2] := mem[$A000:(n1+y)*320+x+n2];
  239.     showsprite(x,y,ofs(MySprite));     { Sprite zeichnen }
  240.     showsprite(x,y,ofs(Buffer));       { HG wiederherstellen }
  241.     BewegeSprite;                      { Neue Koordinaten }
  242.   until Bedingung = true;
  243.   settextmode;
  244. end.
  245.  
  246. Gut, für ein Sprite mag diese Methode ausreichen, aber was ist, wenn man z.B.
  247. 10 Sprites über den HG bewegen will? Tja, jetzt muß Methode 2 herhalten.
  248. Diese benutzt folgendes Schema:
  249.  
  250. begin
  251.   setmcgamode;
  252.   ErzeugeHintergrundAufVS;
  253.   repeat
  254.     KopiereHGAufVGA;
  255.     ZeichneSprites;
  256.   until Bedingung = true;
  257.   settextmode;
  258. end.
  259.  
  260. Moment maaal! Was soll den 'VS' sein? Nun, das ist der Virtual Screen. Also
  261. ein 'virtueller Bildschirm'. Man kann ihn beschreiben und auslesen wie die
  262. VGA. Aber das Wichtige ist: Man kann ihn auf die VGA kopieren, so, daß sein
  263. Inhalt auf dem Bildschirm erscheint.
  264. Man erstellt also einen Virtual Screen (wie, werden wir noch sehen), und
  265. erstellt einen Hintergrund auf ihm. Dann startet man die Schleife und kopiert
  266. den Hintergrund aus dem VS auf die VGA. Nun zeichnet man seine Sprites darauf.
  267. Aber wie erstelle ich nun so ein Teil???
  268. Am einfachsten wäre wohl die folgende Lösung:
  269.  
  270. var VS : array[0..63999] of byte;
  271.  
  272. Diese Variable könnte nun wie ein zweiter VGA-Bildschirm benutzt werden, aber:
  273. Das Array ist 64000 Byte groß. Da man aber unter Pascal für globale Variablen
  274. nur maximal 65000 und ein paar Byte zur Verfügung hat, würde es sehr eng
  275. werden, und schnell kriegen wir die Compiler-Meldung 'Zuviele Variablen'.
  276. Die Lösung für unser Problem sind also: Zeiger (Pointer).
  277. Die Deklaration des VS muß also so aussehen:
  278.  
  279. type BigArr = array[0..63999] of byte;
  280.      VSPtr = ^BigArr;
  281.  
  282. var VS : VSPtr;
  283.     VSAdd : word;
  284.  
  285. Nun müssen wir den VS nur noch initialisieren:
  286.  
  287. procedure InitVS;
  288. begin
  289.   getmem(VS,64000);
  290.   VSAdd := seg(VS^);
  291. end;
  292.  
  293. Nun haben wir für den VS 64000 Byte an Speicher allokiert. Um ihn wieder
  294. freizugeben sollte folgende Prozedur benutzt werden:
  295.  
  296. procedure CloseVS;
  297. begin
  298.   freemem(VS,64000);
  299. end;
  300.  
  301. Will man nun auf den VS statt auf die VGA schreiben, ersetzt man die Adresse
  302. des Bildschirmspeichers 'A000h:0' durch 'VSAdd:0'. Mit dem Mem-Befehl sähe das
  303. so aus:
  304.  
  305. x := 50;
  306. y := 80;
  307. mem[VSAdd:y*320+x] := Col;
  308.  
  309. setzt auf dem VS an die Koordinaten (50,80) einen Pixel der Farbe Col.
  310. Was aber nützt mir das? Nun, nachdem wir den Hintergrund auf diese Weise auf
  311. den VS gezeichnet haben, können wir ihn mit der folgenden Prozedur auf den
  312. VGA-Screen kopieren:
  313.  
  314. procedure Flip;assembler;
  315. asm
  316.   push    ds                           { DS MUß gesichert werden }
  317.   mov     ax,0A000h                    { Zielsegment nach ES }
  318.   mov     es,ax
  319.   mov     ds,VSAdd                     { Quellsegment nach DS }
  320.   xor     si,si                        { Offset = 0 }
  321.   xor     di,di
  322.   mov     cx,32000                     { 32000 Word (=64000 Byte) }
  323.   rep     movsw                        { kopieren }
  324.   pop     ds                           { DS wiederherstellen }
  325. end;
  326.  
  327. Nun werden, wie besprochen, die Sprites darüber gezeichnet, was natürlich
  328. entsprechend flott vonstatten gehen sollte. Um Flickern und 'zerrissene'
  329. Sprites zu vermeiden, sollte man außerdem noch die WaitRetrace-Prozedur
  330. aufrufen.
  331. Wer das mit dem Virtual Screen noch nicht ganz gepeilt hat (ist nicht einfach,
  332. geb ich zu), der sei getröstet, ab der nächsten Ausgabe brauchen wir so was
  333. nicht mehr, denn dann besprechen wir den VGA-Modus, der von Hause aus
  334. 4 Virtual Screens mitbringt, ohne auch nur ein Byte mehr Speicher zu
  335. verbrauchen: Der Mode-X!
  336. Also, ciao bis zum nächsten Teil.
  337.  
  338.  
  339.  
  340.  
  341. [ This text copyright (c) 1995-96 Johannes Spohr. All rights reserved. ]
  342. [ Distributed exclusively through PC-Heimwerker, Verlag Thomas Eberle. ]
  343. [                                                                      ]
  344. [ No  part   of  this   document  may  be   reproduced,   transmitted, ]
  345. [ transcribed,  stored in a  retrieval system,  or translated into any ]
  346. [ human or computer language, in any form or by any means; electronic, ]
  347. [ mechanical,  magnetic,  optical,   chemical,  manual  or  otherwise, ]
  348. [ without the expressed written permission of the author.              ]
  349. [                                                                      ]
  350. [ The information  contained in this text  is believed  to be correct. ]
  351. [ The text is subject to change  without notice and does not represent ]
  352. [ a commitment on the part of the author.                              ]
  353. [ The author does not make a  warranty of any kind with regard to this ]
  354. [ material, including,  but not limited to,  the implied warranties of ]
  355. [ merchantability  and fitness  for a particular  purpose.  The author ]
  356. [ shall not be liable for errors contained herein or for incidental or ]
  357. [ consequential damages in connection with the furnishing, performance ]
  358. [ or use of this material.                                             ]
  359.